<H2>Slime Sim</H2>
<canvas id="canvas" width="500" height="500" style="background:black"></canvas>
<script>
  const canvas = document.getElementById("canvas"); 
  const ctx = canvas.getContext("2d");
  const PI = Math.PI;
  const TPI = PI * 2;
  const W = canvas.width, H = canvas.height;
  const HW = W/2, HH = H/2;
  const SANGLE = 30 * Math.PI / 180.0;
  const SSTEP = 35, SSIZE = 1;
  const DLTTIME = 0.02;
  const TSTEP = 2 * TPI * DLTTIME;
  const DIFFR = 3 * DLTTIME , DECAR = 0.3 * DLTTIME;
  const STEP = 3, TW = 5 * DLTTIME;
  
  const SPECIES = [{
      sensor_radian:RAD(30),
      sensor_offset:20,
      sensor_width:1,
      turn_step:RAD(14),
      step:1,
      diff_rate:0.06,
      decay_rate:3,
      trail_inc:35,
      color:[0.53608304, 0.062745094, 1]
  }, {
      sensor_radian:RAD(70),
      sensor_offset:30,
      sensor_width:1,
      turn_step:RAD(14),
      step:1,
      diff_rate:0.06,
      decay_rate:3,
      trail_inc:35,
      color:[0.47160125, 0.8301887, 0.105731584]
  }]

  const N_SPECIES = SPECIES.length;

  function RAD(degree) {
      return degree * PI / 180;
  }
  
  function random_dir() {
      return Math.random() * TPI;
  }
  
  function tindex(x, y, sindex) {
      return (sindex >= N_SPECIES || x < 0 || x >= W || y < 0 || y >= H) ?
	  -1 : (y * W + x) * N_SPECIES + sindex;
  }

  function forward(x, y, s, a) {
      return [x + s * Math.cos(a), y - s * Math.sin(a)];
  }
  
  function clamp(v, min, max) {
      return Math.max(min, Math.min(max,v));
  }

  function tv(tmap, x, y, sindex) {
      const index = tindex(x,y,sindex);
      return index < 0 ? 0 : tmap[index];
  }

  function diffuse(input, output) {
      for(let s = 0; s < N_SPECIES; ++s) {
	  const ss = SPECIES[s];
	  const diff = ss.diff_rate;
	  const decay = ss.decay_rate;
	  for(let x = 0; x < W; ++x) {
	      for(let y = 0; y < H; ++y) {
		  const index = tindex(x,y,s);
		  const ori =input[index]; 
		  let v = ori;
		  v += tv(input,x-1,y-1,s);
		  v += tv(input,x,y-1,s);
		  v += tv(input,x+1,y-1,s);
		  v += tv(input,x-1,y,s);
		  v += tv(input,x+1,y,s);
		  v += tv(input,x-1,y+1,s);
		  v += tv(input,x,y+1,s);
		  v += tv(input,x+1,y+1,s);
		  v = v/9.0;
		  const nt = v * diff + ori * (1-diff);
		  output[index] = Math.max(0, Math.floor(nt - decay)); 
	      }
	  }
      }
  }

  function Agent(index) {
      this.species_index = index;
      this.settings = SPECIES[index];
      
      this.init = function(x,y,radian) {
	  this.x = x;
	  this.y = y;
	  this.dir = radian;
	  return this;
      };

      this.init_center_outward = function() {
	  this.x = HW;
	  this.y = HH;
	  this.dir = random_dir();
	  return this;
      };

      this.init_center_inward = function() {
	  var r = random_dir();
	  var l = HW * Math.random();
	  this.x = HW + l * Math.cos(r);
	  this.y = HH - l * Math.sin(r);
	  this.dir = r + PI;
	  return this;
      };
      
      this.sense = function(tmap, radian) {
	  let [x,y] = forward(this.x,
			      this.y,
			      this.settings.sensor_offset,
			      this.dir + radian);
	  let sum = 0;
	  const ss = this.settings.sensor_width;
	  const sindex = this.species_index
	  x = Math.floor(x);
	  y = Math.floor(y);
	  for(let i = -ss; i <= ss; ++i) {
	      for(let j = -ss; j <= ss; ++j) {
		  sum += tv(tmap,x+i,y+i,sindex);
	      }
	  }
	  return sum;
      };

      this.tindex = function() {
	  return tindex(Math.floor(this.x), Math.floor(this.y), this.species_index);
      };
      
      this.update = function(tmap) {
	  const sr = this.settings.sensor_radian;
	  const ts = this.settings.turn_step;
	  const fw = this.sense(tmap, 0),
		lw = this.sense(tmap, sr),
		rw = this.sense(tmap, -sr);
	  const r = Math.random();
	  if(fw < lw && fw < rw) {
	      this.dir += (r - 0.5) * 2 * ts;
	  } else if(lw < rw) {
	      this.dir -= r * ts; 
	  } else if(lw > rw) {
	      this.dir += r * ts; 
	  } 
	  
	  let [x,y] = forward(this.x, this.y, this.settings.step, this.dir);
	  
	  if( x <= 0 || x >= W-1 || y <= 0 || y >= H-1) {
	      this.dir = random_dir();
	      this.x = clamp(x,0,W-1);
	      this.y = clamp(y,0,H-1);
	  } else{
	      this.x = x;
	      this.y = y;
	  }
	  let index = this.tindex();
	  tmap[index] = Math.min(255, Math.floor(tmap[index] + this.settings.trail_inc));
	  return this;
      };
  }
  
  let TMapRead = new Uint8Array(W*H*N_SPECIES);
  let TMapWrite = new Uint8Array(W*H*N_SPECIES);
  const TOTAL = 10000;
  const agents = new Array(TOTAL);
  const timage = ctx.createImageData(W, H);
  for(let i = 0; i < TOTAL; ++i) {
      agents[i] = new Agent(Math.random() > 0.7? 0 : 1).init_center_inward();
  }

  function init_timage(data) {
      for(let i = 0; i < TOTAL; ++i) {
	  const a = agents[i];
	  const index = (Math.floor(a.y) * W + Math.floor(a.x)) * 4;
	  data[index+3] = 255;
      }
  }

  function add_color(sum, c, v) {
      sum[0] += c[0] * v;
      sum[1] += c[1] * v;
      sum[2] += c[2] * v;
      sum[3] += v;
  }

  function vcolor(v) {
      return Math.min(Math.floor(v * 255), 255);
  }
  
  function update_timage(tmap, data) {
      for(let x = 0; x < W; ++x) {
	  for(let y = 0; y < H; ++y) {
	      let v = [0,0,0,0];
	      for(let s = 0; s < N_SPECIES; ++s) {
		  add_color(v,SPECIES[s].color,tv(tmap,x,y,s));
	      }
	      let i = (y * W + x) * 4;
	      //data[i+3] = Math.max(0, 255-v);
	      data[i] =v[0];
	      data[i+1] =v[1];
	      data[i+2] =v[2];
	      data[i+3] = v[3];
	  }
      }
  }
 
  function update() {
      for(let i = 0; i < agents.length; ++i) {
	  agents[i].update(TMapRead);
      }
      diffuse(TMapRead, TMapWrite);
      update_timage(TMapWrite, timage.data);
      ctx.putImageData(timage,0,0);
      var t = TMapRead;
      TMapRead = TMapWrite;
      TMapWrite = TMapRead;
      requestAnimationFrame(update);
  }
  //init_timage(timage.data);
  //ctx.putImageData(timage,0,0);
  update();
</script>
